import classNames from 'classnames'

function filterProps (props, propsData = {}) {
  const res = {}
  Object.keys(props).forEach(k => {
    if (k in propsData || props[k] !== undefined) {
      res[k] = props[k]
    }
  })
  return res
}

export const getPropsData = ele => {
  let componentOptions = ele.componentOptions
  if (ele.$vnode) {
    componentOptions = ele.$vnode.componentOptions
  }
  return componentOptions ? componentOptions.propsData || {} : {}
}

export function getListeners (context) {
  return (context.$vnode ? context.$vnode.componentOptions.listeners : context.$listeners) || {}
}

export const getScopedSlots = ele => {
  return (ele.data && ele.data.scopedSlots) || {}
}

function getType (fn) {
  const match = fn && fn.toString().match(/^\s*function (\w+)/)
  return match ? match[1] : ''
}

export const getComponentFromProp = (
  instance,
  prop,
  options = instance,
  execute = true
) => {
  if (instance.$createElement) {
    const h = instance.$createElement
    const temp = instance[prop]
    if (temp !== undefined) {
      return typeof temp === 'function' && execute ? temp(h, options) : temp
    }
    return (
      (instance.$scopedSlots[prop] &&
        execute &&
        instance.$scopedSlots[prop](options)) ||
      instance.$scopedSlots[prop] ||
      instance.$slots[prop] ||
      undefined
    )
  } else {
    const h = instance.context.$createElement
    const temp = getPropsData(instance)[prop]
    if (temp !== undefined) {
      return typeof temp === 'function' && execute ? temp(h, options) : temp
    }
    const slotScope = getScopedSlots(instance)[prop]
    if (slotScope !== undefined) {
      return typeof slotScope === 'function' && execute
        ? slotScope(h, options)
        : slotScope
    }
    const slotsProp = []
    const componentOptions = instance.componentOptions || {};
    (componentOptions.children || []).forEach(child => {
      if (child.data && child.data.slot === prop) {
        if (child.data.attrs) {
          delete child.data.attrs.slot
        }
        if (child.tag === 'template') {
          slotsProp.push(child.children)
        } else {
          slotsProp.push(child)
        }
      }
    })
    return slotsProp.length ? slotsProp : undefined
  }
}

export const hasProp = (instance, prop) => {
  const $options = instance.$options || {}
  const propsData = $options.propsData || {}
  return prop in propsData
}

export const initDefaultProps = (propTypes, defaultProps) => {
  Object.keys(defaultProps).forEach(k => {
    if (propTypes[k]) {
      propTypes[k].def && (propTypes[k] = propTypes[k].def(defaultProps[k]))
    } else {
      throw new Error(`not have ${k} prop`)
    }
  })
  return propTypes
}

export const getOptionProps = instance => {
  if (instance.componentOptions) {
    const componentOptions = instance.componentOptions
    const {propsData = {}, Ctor = {}} = componentOptions
    const props = (Ctor.options || {}).props || {}
    const res = {}
    for (const [k, v] of Object.entries(props)) {
      const def = v.default
      if (def !== undefined) {
        res[k] =
          typeof def === 'function' && getType(v.type) !== 'Function'
            ? def.call(instance)
            : def
      }
    }
    return {...res, ...propsData}
  }
  const {$options = {}, $props = {}} = instance
  return filterProps($props, $options.propsData)
}

export function isEmptyElement (c) {
  return !(c.tag || (c.text && c.text.trim() !== ''))
}

export function filterEmpty (children = []) {
  return children.filter(c => !isEmptyElement(c))
}

export function findParent (context, componentName) {
  let parent = context.$parent
  let parentName = context.$parent.$options.componentName
  while (parent && parentName !== componentName) {
    parent = parent.$parent
    parentName = parent && parent.$options.componentName
  }
  return parent
}

export function findComponentUpward (context, componentName, componentNames) {
  if (typeof componentName === 'string') {
    componentNames = [componentName]
  } else {
    componentNames = componentName
  }

  let parent = context.$parent
  let name = parent.$options.name
  while (parent && (!name || componentNames.indexOf(name) < 0)) {
    parent = parent.$parent
    if (parent) name = parent.$options.name
  }
  return parent
}

export function findComponentDownward (context, componentName) {
  const childrens = context.$children
  let children = null

  if (childrens.length) {
    for (const child of childrens) {
      const name = child.$options.name
      if (name === componentName) {
        children = child
        break
      } else {
        children = findComponentDownward(child, componentName)
        if (children) break
      }
    }
  }
  return children
}

export function findComponentsDownward (
  context,
  componentName,
  ignoreComponentNames = []
) {
  if (!Array.isArray(ignoreComponentNames)) {
    ignoreComponentNames = [ignoreComponentNames]
  }
  return context.$children.reduce((components, child) => {
    if (child.$options.name === componentName) components.push(child)
    if (ignoreComponentNames.indexOf(child.$options.name) < 0) {
      const foundChilds = findComponentsDownward(child, componentName)
      return components.concat(foundChilds)
    } else {
      return components
    }
  }, [])
}

export function findComponentsUpward (context, componentName) {
  let parents = []
  const parent = context.$parent
  if (parent) {
    if (parent.$options.name === componentName) parents.push(parent)
    return parents.concat(findComponentsUpward(parent, componentName))
  } else {
    return []
  }
}

export function findBrothersComponents (
  context,
  componentName,
  exceptMe = true
) {
  let res = context.$parent.$children.filter(item => {
    return item.$options.name === componentName
  })
  let index = res.findIndex(item => item._uid === context._uid)
  if (exceptMe) res.splice(index, 1)
  return res
}

const camelizeRE = /-(\w)/g
const camelize = str => {
  return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
}

const parseStyleText = (cssText = '', camel) => {
  const res = {}
  const listDelimiter = /;(?![^(]*\))/g
  const propertyDelimiter = /:(.+)/
  cssText.split(listDelimiter).forEach(function (item) {
    if (item) {
      const tmp = item.split(propertyDelimiter)
      if (tmp.length > 1) {
        const k = camel ? camelize(tmp[0].trim()) : tmp[0].trim()
        res[k] = tmp[1].trim()
      }
    }
  })
  return res
}

export function cloneVNode (vnode, deep) {
  const componentOptions = vnode.componentOptions
  const data = vnode.data

  let listeners = {}
  if (componentOptions && componentOptions.listeners) {
    listeners = {...componentOptions.listeners}
  }

  let on = {}
  if (data && data.on) {
    on = {...data.on}
  }

  const cloned = new vnode.constructor(
    vnode.tag,
    data ? {...data, on} : data,
    vnode.children,
    vnode.text,
    vnode.elm,
    vnode.context,
    componentOptions ? {...componentOptions, listeners} : componentOptions,
    vnode.asyncFactory
  )
  cloned.ns = vnode.ns
  cloned.isStatic = vnode.isStatic
  cloned.key = vnode.key
  cloned.isComment = vnode.isComment
  cloned.fnContext = vnode.fnContext
  cloned.fnOptions = vnode.fnOptions
  cloned.fnScopeId = vnode.fnScopeId
  cloned.isCloned = true
  if (deep) {
    if (vnode.children) {
      cloned.children = cloneVNodes(vnode.children, true)
    }
    if (componentOptions && componentOptions.children) {
      componentOptions.children = cloneVNodes(componentOptions.children, true)
    }
  }
  return cloned
}

export function cloneVNodes (vnodes, deep) {
  const len = vnodes.length
  const res = new Array(len)
  for (let i = 0; i < len; i++) {
    res[i] = cloneVNode(vnodes[i], deep)
  }
  return res
}

export function cloneElement (n, nodeProps = {}, deep) {
  let ele = n
  if (Array.isArray(n)) {
    ele = filterEmpty(n)[0]
  }
  if (!ele) {
    return null
  }
  const node = cloneVNode(ele, deep)
  const {props = {}, key, on = {}, children, directives = []} = nodeProps
  const data = node.data || {}
  let cls = {}
  let style = {}
  const {
    attrs = {},
    ref,
    domProps = {},
    style: tempStyle = {},
    class: tempCls = {},
    scopedSlots = {}
  } = nodeProps

  if (typeof data.style === 'string') {
    style = parseStyleText(data.style)
  } else {
    style = {...data.style, ...style}
  }
  if (typeof tempStyle === 'string') {
    style = {...style, ...parseStyleText(style)}
  } else {
    style = {...style, ...tempStyle}
  }

  if (typeof data.class === 'string' && data.class.trim() !== '') {
    data.class.split(' ').forEach(c => {
      cls[c.trim()] = true
    })
  } else if (Array.isArray(data.class)) {
    classNames(data.class)
      .split(' ')
      .forEach(c => {
        cls[c.trim()] = true
      })
  } else {
    cls = {...data.class, ...cls}
  }
  if (typeof tempCls === 'string' && tempCls.trim() !== '') {
    tempCls.split(' ').forEach(c => {
      cls[c.trim()] = true
    })
  } else {
    cls = {...cls, ...tempCls}
  }
  node.data = Object.assign({}, data, {
    style,
    attrs: {...data.attrs, ...attrs},
    class: cls,
    domProps: {...data.domProps, ...domProps},
    scopedSlots: {...data.scopedSlots, ...scopedSlots},
    directives: [...(data.directives || []), ...directives]
  })

  if (node.componentOptions) {
    node.componentOptions.propsData = node.componentOptions.propsData || {}
    node.componentOptions.listeners = node.componentOptions.listeners || {}
    node.componentOptions.propsData = {
      ...node.componentOptions.propsData,
      ...props
    }
    node.componentOptions.listeners = {
      ...node.componentOptions.listeners,
      ...on
    }
    if (children) {
      node.componentOptions.children = children
    }
  } else {
    node.data.on = {...(node.data.on || {}), ...on}
  }

  if (node.fnOptions && node.fnOptions.functional) {
    node.data.on = {...(node.data.on || {}), ...on}
  }

  if (key !== undefined) {
    node.key = key
    node.data.key = key
  }
  if (typeof ref === 'string') {
    node.data.ref = ref
  }
  return node
}
